<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebRTC 视频传输</title>
  <script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
</head>
<body>
  <h1>WebRTC 视频传输</h1>
  <button id="startCamera">启动摄像头</button>
  <button id="startStreaming" disabled>开始推流</button>
  <button id="joinSpecificRoom">加入房间</button>
  <video id="remoteVideo" autoplay></video>
  <video id="localVideo" autoplay muted></video>

  <script>
    const SIGNALING_SERVER = 'wss://1.94.180.78:3000';
    const TURN_CONFIG = {
      iceServers: [
        {
          urls: [
            'turn:1.94.180.78:3478?transport=udp',
            'turn:1.94.180.78:3478?transport=tcp'
          ],
          username: 'daji',
          credential: '123456'
        },
        {
          urls: 'stun:1.94.180.78:3478'
        }
      ]
    };

    const localVideo = document.getElementById('localVideo');
    const remoteVideo = document.getElementById('remoteVideo');
    const startCameraButton = document.getElementById('startCamera');
    const startStreamingButton = document.getElementById('startStreaming');
    const joinSpecificRoomButton = document.getElementById('joinSpecificRoom');

    startCameraButton.onclick = startCamera;
    startStreamingButton.onclick = startStreaming;
    joinSpecificRoomButton.onclick = joinSpecificRoom;

    // WebSocket连接
    const sock = io(SIGNALING_SERVER); // 连接到你的服务器

    const roomId = '123';
    let socket;
    let localStream;  // 本地视频流
    let remoteStream;  // 远程视频流

    // 该用户全局唯一对话Id，用于区分不同用户
    let conversationId = "";

    // 新增：只有发送方会用到。接收方请无视这玩意。发送方会创建多个 peerConnection，他们都会存放在这里面
    // 每一个peer，对应一个接收方
    const peerConnections = new Map(); 

    // 接收服务端发的自定义事件
    sock.on('connectionSuccess', (fromSignalingServerId) => {
      // 初始化赋值全局唯一对话Id，用于区分不同用户
      conversationId = fromSignalingServerId;
      console.log('客户端:'+conversationId+'连接成功');
      // 向服务器发送一个加入房间的事件
      sock.emit("joinRoom", roomId, conversationId);
    });

    // 2025年10月26日22:52:48 大吉：注释掉该处。自动接收就行，客户端只需要加入房间即可。
    // sock.on('callRemote', (fromSignalingServerId) => {
    //   // 注意！！这个fromSignalingServerId不是自己的。这个fromSignalingServerId是发送方的fromSignalingServerId
    //   // 只有接收方才有权监听到该请求。
    //     console.log("监听到发送方发起视频请求，发送方id："+fromSignalingServerId+"，接收方id："+conversationId);
    // });


    sock.on('acceptCall',async (fromSignalingServerId) => {

      if (fromSignalingServerId !== conversationId) {
        console.log("接收到acceptCall,但是不是发给我的，直接跳过。 是发给："+fromSignalingServerId);
        return;
      }
      
      console.log("发送方收到接收方同意接收视频的请求，接收方id："+fromSignalingServerId);
      // 发送方建立 RTCPeerConnection
      let peer = new RTCPeerConnection(TURN_CONFIG);
      // 极其重要的一步，不同的接收方id，创建不同的 peer
      peerConnections.set(conversationId,peer);
      // 添加本地音视频流，生成offer
      localStream.getTracks().forEach(track => {
        peer.addTrack(track, localStream);
      });
      console.log("发送方已添加本地视频流");

      // 通过监听onicecandidate事件获取icecandidate信息
      // ‌ICE候选地址（ICE Candidate）‌是Interactive Connectivity Establishment协议中用于建立网络连接的传输地址，包含IP地址、端口、传输协议以及优先级等属性，主要用于解决NAT穿透问题。‌‌
      peer.onicecandidate = (event) => {
        // console.log('用户A的candidate信息：'+event.candidate);
        if (event.candidate) {
          // 向信令服务器发送 发送方的candidate信息
          socket.emit('sendCandidate', event.candidate, roomId, fromSignalingServerId )
          console.log("已经向信令服务器发送了发送方的candidate信息，其中包含了发送方id："+fromSignalingServerId);

        }
      }

      // 创建offer
      const offer = await peer.createOffer({
        offerToReceiveAudio: 1,
        offerToReceiveVideo: 1
      })
      console.log("offer is: "+JSON.stringify(offer));

      // 在本地设置offer信息(这里我还不太理解)：
      // 运用await，就是在等待结果，因为 async 函数返回一个 Promise 对象，所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数，但要清楚，它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象，它可以等任意表达式的结果，所以，await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行。
      // setLocalDescription本身是异步的，为了确保完全成功设置，所以需要加await
      await peer.setLocalDescription(offer);
      

      // 发送offer：
      // TODO 这里为啥用 socket？我个人认为是因为这个方法是异步的。可能拿不到sock。当然，sock就是socket
      // 把fromSignalingServerId（接收方id）的offer发出去
      socket.emit('sendOffer',offer,roomId,fromSignalingServerId)
      
      
    });

    // 接收方收到offer
    sock.on('sendOffer',async (offer, fromSignalingServerId) =>{
      // 只有接收方才有权接收视频流，而且是你接收的，不是你的你别抢。
      if (fromSignalingServerId !== conversationId) {
        console.log("接收方的确收到offer了，但是不是我接收的。我自己的fromSignalingServerId是："+conversationId+"，这次offer发给的fromSignalingServerId是："+fromSignalingServerId);
      }
      if (fromSignalingServerId === conversationId) {
        console.log("接收方接收到发送方的推视频流请求："+JSON.stringify(offer));
        console.log("此时接收方id："+fromSignalingServerId+"，接收方id："+conversationId+"，这两个应该相等！！");

        // 第一步：接收方必须在本地存一下这个offer，然后再进行其他操作。
        // 接收方必须创建属于自己的RTCPeerConnection，添加本地视频流，设置远端描述信息，生成answer，并且把这个answer通过信令服务器发给发送方。
        let peer = new RTCPeerConnection(TURN_CONFIG);
        // 极其重要的一步，不同的接收方id，创建不同的 peer
        peerConnections.set(conversationId,peer);
        // 添加本地音视频流 (虽然接收方没有Camera，我们可以添加一个空的假的音视频流)
        await startEmptyCamera();
        // peer.addStream(stream);

        // 添加本地音视频流(是个空的玩意)
        localStream.getTracks().forEach(track => {
          peer.addTrack(track, localStream);
        });

        // 接收方发送自己的candidate信息
        peer.onicecandidate = (event) => {
          if (event.candidate) {
            // 向信令服务器发送 接收方的candidate信息
            socket.emit('sendCandidate', event.candidate, roomId, fromSignalingServerId)
            console.log("已经向信令服务器发送了接收方的candidate信息！，其中包含了接收方id："+fromSignalingServerId);

          }
        }

        // 接收方通过监听onTrack来获取对方的音视频流
        peer.ontrack = (event) => {
          console.log("接收方收到发送方的视频流："+event.streams[0]);
          remoteVideo.srcObject = event.streams[0];
          remoteVideo.play();
        }


        // 设置远端描述信息 SDP
        await peer.setRemoteDescription(offer)
        // 生成answer
        const answer = await peer.createAnswer()
        console.log("接收方已生成answer："+JSON.stringify(answer));
        // 在本地设置answer信息：
        await peer.setLocalDescription(answer)
        // 发送answer
        console.log("接收方发送answer，接收方id："+fromSignalingServerId);
        socket.emit('sendAnswer',  answer, roomId ,fromSignalingServerId)
      }
    });

    // 发送方收到answer
    sock.on('sendAnswer', async (answer, fromSignalingServerId) =>{
      
      console.log("发送方收到answer："+JSON.stringify(answer));
      console.log("发送方的answer收到了接收方id："+fromSignalingServerId);
      debugger
      // FIXME 这种方式收到answer可能有问题！！
      // 如果报错，改成这一行：await peer.setRemoteDescription(new RTCSessionDescription(answer))
      // 极其重要，发送方给不同的接收方发送answer
      await peerConnections.get(conversationId).setRemoteDescription(answer)
      console.log("发送方设置远程answer成功！，是给这个接收方id设置的："+fromSignalingServerId);
    });

    // 收到candidate
    sock.on('sendCandidate', async (candidate, fromSignalingServerId, senderConversationIds, receiverConversationIds) =>{
      debugger
      // 如果我是接收方我该怎么办
      // 我会判断 fromSignalingServerId 到底是接收方还是发送方，如果是发送方，我会存下它的candidate
      peerConnections.get(conversationId).addIceCandidate(new RTCIceCandidate(candidate))


      // 如果是发送方收到candidate，正常运行
      peerConnections.get(fromSignalingServerId).addIceCandidate(new RTCIceCandidate(candidate))
      console.log('发送方收到candidate。接收方id：'+fromSignalingServerId+"，发送方id："+conversationId+"，接收方id和发送方id不等。");
      
      // 如果是接收方收到candidate，得看是不是发给它的
      if (fromSignalingServerId !== conversationId) {
        console.log("接收方的确收到candidate了，但是不是我接收的。我自己的fromSignalingServerId是："+conversationId+"，这次candidate发给的fromSignalingServerId是："+fromSignalingServerId);
      }
      if (fromSignalingServerId === conversationId) {
        // 极其重要，接收方收到不同的peer的candidate
        peerConnections.get(fromSignalingServerId).addIceCandidate(new RTCIceCandidate(candidate))
        console.log('接收方可以收到信令服务器传来的candidate。此时不出意外的话，接收方id：'+fromSignalingServerId+"，接收方id："+conversationId+"，这两者相等！");
      }

    });


    // 存一下这个sock
    socket = sock;


    // 接收方无需任何摄像头，但是也要返回一个空的假的音视频流
    async function startEmptyCamera() {
      try {
        // 只发视频，不要音频
        // 这里的await也是等待异步方法调用完成
        const stream = await new MediaStream();
        localVideo.srcObject = stream;
        localVideo.play();
        localStream = stream;

        console.log("接收方开启假摄像头（一个流）成功");
      } catch (error) {
        console.error('接收方开启假摄像头（一个流）失败： ', error);
      }
    }

    // 启动摄像头
    async function startCamera() {
      try {
        // 这里的await也是等待异步方法调用完成
        const stream = await navigator.mediaDevices.getUserMedia({
          video: {
            facingMode: "environment", // 指定使用后置摄像头
            frameRate: { ideal: 30 } // 降低帧率，减少数据量
          }, 
           audio: true 
        });
        localVideo.srcObject = stream;
        localVideo.play();
        localStream = stream;

        startStreamingButton.disabled = false;
        console.log("开启摄像头成功");
      } catch (error) {
        console.error('Error accessing camera: ', error);
      }
    }

    // 开始推流
    function startStreaming() {

      // 通过信令服务器向用户B发起视频请求：
      socket.emit('callRemote', roomId, conversationId);
    }

    // 接收方决定加入某房间
    function joinSpecificRoom(roomId) {
      roomId = '123';
      console.log("接收方同意视频邀请");
      // 获取该房间所有的发送方id
      socket.emit('getAllSenderConversationIds',roomId);

      socket.on('getAllSenderConversationIds', (senderConversationIds) => {
        console.log('已获取所有发送方id，所有发送方id为：'+JSON.stringify(senderConversationIds));

        // 通过信令服务器通知所有发送方
        for (let sender of senderConversationIds) {
            socket.emit('acceptCall', roomId, sender);
        }
        console.log('acceptCall已经全部通知到所有发送方。');
      })
      
    }

    // 添加vconsole
    let script = document.createElement("script");
    script.src = "https://cdn.bootcss.com/vConsole/3.3.4/vconsole.min.js";
    script.onload = function() { new VConsole(); };
    document.head.appendChild(script);
  </script>
</body>
</html>
